/* --------------------------------------------------------
   dreamcast/main.c
   Main loop/emulation code for Genesis Plus / DC

   Dreamcast code by BlackAura (obsidianglow@hotpop.com)
   Genesis Plus by Charles MacDonald (cgfm2@hotmail.com)
   -------------------------------------------------------- */

#include <kos.h>
#include "shared.h"
#include "menu.h"
#include "font.h"

static void *screen_buffer;		/* Main display - Buffer */
static pvr_ptr_t gen_texptr;		/* Main display - Texture */
static pvr_poly_hdr_t gen_hdr;		/* Main display - Poly header */

#include "xrender.h"
#include "xsystem.h"

#define SRC_TOP		(0.0f)
#define SRC_BUM		(0.875f)
#define SRC_LEFT	((float)(bitmap.viewport.x) / (float)(TEX_WIDTH))
#define SRC_RIGHT	((float)(bitmap.viewport.x + bitmap.viewport.w) / (float)(TEX_WIDTH))

/* Sound system */
#include <dc/spu.h>
#include <dc/g2bus.h>
#include <dc/sound/sound.h>
#include "arm/aica_cmd_iface.h"

#define SPU_RAM_BASE            0xa0800000

#define BYTES_PER_BLOCK		32
#define BYTES_PER_SAMPLE	2
#define SAMPLES_PER_BLOCK	(BYTES_PER_BLOCK / BYTES_PER_SAMPLE)
#define BLOCKS_PER_FRAME	12
#define SAMPLES_PER_FRAME	(SAMPLES_PER_BLOCK * BLOCKS_PER_FRAME)
#define BYTES_PER_FRAME		(SAMPLES_PER_FRAME * BYTES_PER_SAMPLE)
#define SAMPLES_PER_SECOND	(SAMPLES_PER_FRAME * 60)

#define DCSND_BUFFERS		6
#define DCSND_SKIP_DISTANCE	3
#define DCSND_BUFFERS_BYTES	(DCSND_BUFFERS * BYTES_PER_FRAME)
#define DCSND_BUFFERS_SAMPLES	(DCSND_BUFFERS * SAMPLES_PER_FRAME)

static uint32 dcsnd_sram_ptr[2];

static int dcsnd_current_buffer;
static int dcsnd_inited = 0;

int frames_skipped = 0;

void dcsnd_init()
{
	if(dcsnd_inited)
		return;

	/* Initialise the SPU */
	snd_init();

	/* Allocate stereo output buffers */
	dcsnd_sram_ptr[0] = snd_mem_malloc(DCSND_BUFFERS_BYTES);
	dcsnd_sram_ptr[1] = snd_mem_malloc(DCSND_BUFFERS_BYTES);

	dcsnd_inited = 1;
}
  
void dcsnd_shutdown()
{
	/* It can't be shut down! */
}

void dcsnd_play()
{
	AICA_CMDSTR_CHANNEL(tmp, cmd, chan);

	flashrom_syscfg_t syscfg;
	int left_pan = 0;
	int right_pan = 255;

	// If the Dreamcast's sound settings are set to mono, disable panning
	flashrom_get_syscfg(&syscfg);
	if(syscfg.audio == 0) {
		left_pan = right_pan = 128;
	}

	if( (!dcsnd_inited) || (!snd.enabled) )
		return;

	/* Make sure these are sync'd (and/or delayed) */
	snd_sh4_to_aica_stop();

	/* Set up channel 0 */
	cmd->cmd = AICA_CMD_CHAN;
	cmd->timestamp = 0;
	cmd->size = AICA_CMDSTR_CHANNEL_SIZE;
	cmd->cmd_id = 0;
	chan->cmd = AICA_CH_CMD_START | AICA_CH_START_DELAY;
	chan->base = dcsnd_sram_ptr[0];
	chan->type = AICA_SM_16BIT;
	chan->length = DCSND_BUFFERS_SAMPLES;
	chan->loop = 1;
	chan->loopstart = 0;
	chan->loopend = DCSND_BUFFERS_SAMPLES;
	chan->freq = SAMPLES_PER_SECOND;
	chan->vol = 255;
	chan->pan = left_pan;
	snd_sh4_to_aica(tmp, cmd->size);

	/* Set up channel 1 */
	cmd->cmd_id = 1;
	chan->base = dcsnd_sram_ptr[1];
	chan->pan = right_pan;
	snd_sh4_to_aica(tmp, cmd->size);

	/* Send synchronised start command */
	cmd->cmd_id = (1 << 0) | (1 << 1);
	chan->cmd = AICA_CH_CMD_START | AICA_CH_START_SYNC;
	snd_sh4_to_aica(tmp, cmd->size);

	/* Set up streaming state */
	dcsnd_current_buffer = DCSND_SKIP_DISTANCE;

	/* Start both sound buffers playing */
	snd_sh4_to_aica_start();
}

void dcsnd_stop()
{
	AICA_CMDSTR_CHANNEL(tmp, cmd, chan);

	if( (!dcsnd_inited) || (!snd.enabled) )
		return;

	/* Channel 0 */
	cmd->cmd = AICA_CMD_CHAN;
	cmd->timestamp = 0;
	cmd->size = AICA_CMDSTR_CHANNEL_SIZE;
	cmd->cmd_id = 0;
	chan->cmd = AICA_CH_CMD_STOP;
	snd_sh4_to_aica(tmp, cmd->size);

	/* Channel 1 */
	cmd->cmd_id = 1;
	snd_sh4_to_aica(tmp, cmd->size);

	/* Wipe out sample data in case the second channel doesn't stop */
	spu_memset(dcsnd_sram_ptr[0], 0, DCSND_BUFFERS_BYTES);
	spu_memset(dcsnd_sram_ptr[1], 0, DCSND_BUFFERS_BYTES);
}

// Returns 1 if the sound system is up to date, 0 if a frameskip is required
int dcsnd_update()
{
	int tmp, play_buffer;
	int buffer_distance;
	uint32 play_position;

	if( (!dcsnd_inited) || (!snd.enabled) )
		return 1;

	/* Copy samples over to SRAM */
	/* TODO - Use DMA */
	tmp = dcsnd_current_buffer * BYTES_PER_FRAME;
	spu_memload(dcsnd_sram_ptr[0] + tmp, (uint8*)snd.buffer[0], BYTES_PER_FRAME);
	spu_memload(dcsnd_sram_ptr[1] + tmp, (uint8*)snd.buffer[1], BYTES_PER_FRAME);

	// Determine current playback buffer
	play_position = g2_read_32(SPU_RAM_BASE + AICA_CHANNEL(0) + offsetof(aica_channel_t, pos));
	play_buffer = (play_position / SAMPLES_PER_FRAME);

	// Calculate the distance (assuming the output is running ahead of the playback)
	if(play_buffer > dcsnd_current_buffer)
		buffer_distance = dcsnd_current_buffer - play_buffer;
	else
		buffer_distance = play_buffer + (DCSND_BUFFERS - dcsnd_current_buffer);

	// Advance the output buffer
	dcsnd_current_buffer++;
	if(dcsnd_current_buffer >= DCSND_BUFFERS)
		dcsnd_current_buffer = 0;

	// Return
	return (buffer_distance < DCSND_SKIP_DISTANCE);
}

/* FPS counter */
fnt_info_t          font;

pvr_init_params_t pvr_params = {
	{ PVR_BINSIZE_16, PVR_BINSIZE_0, PVR_BINSIZE_16, PVR_BINSIZE_0, PVR_BINSIZE_16 },
	512 * 1024
};

maple_device_t *devs[2];

void dc_scan_devices()
{
	devs[0] = maple_enum_type(0, MAPLE_FUNC_CONTROLLER);
	devs[1] = maple_enum_type(1, MAPLE_FUNC_CONTROLLER);
}

/* Initialise the Dreamast-specific code */
void dc_init()
{
	pvr_poly_cxt_t gen_cxt;

	dc_scan_devices();

	/* Initialise the PowerVR hardware */
	#if defined(PAL_SCREEN)
	vid_set_mode(DM_640x480_PAL_IL, PM_RGB565);
	#else
	vid_set_mode(DM_640x480_NTSC_IL, PM_RGB565);
	#endif

	pvr_init(&pvr_params);

	/* Allocate and set up the main display texture */
	gen_texptr = pvr_mem_malloc(TEX_WIDTH*TEX_HEIGHT*2);
	pvr_poly_cxt_txr(&gen_cxt,
		PVR_LIST_OP_POLY,
		PVR_TXRFMT_NONTWIDDLED | PVR_TXRFMT_RGB565,
		TEX_WIDTH,
		TEX_HEIGHT,
		gen_texptr,
		PVR_FILTER_NONE); //TRILINEAR1);
	pvr_poly_compile(&gen_hdr, &gen_cxt);

	/* Set up the drawing buffer */
	screen_buffer = memalign(32, TEX_WIDTH*TEX_HEIGHT*2);
	//bitmap.width  = TEX_WIDTH;
	//bitmap.height = TEX_HEIGHT;
	//bitmap.depth  = 16;
	//bitmap.granularity = 2;
	//bitmap.pitch = (bitmap.width * bitmap.granularity);
	bitmap.data = (uint8 *)screen_buffer;
	bitmap.viewport.w = 256;
	bitmap.viewport.h = 224;
	bitmap.viewport.x = 0x00;
	bitmap.viewport.y = 0x00;
	//bitmap.remap = 1;

	/* Set up the font system, load fonts */
	fnt_load("FN000", &font);

	/* Initialise the error handler */
	error_init();
}

void dc_shutdown()
{
	/* Free fonts, shut down PVR */
	fnt_unload(&font);
	pvr_shutdown();
}

/* Maple scanning code */
static __inline__ int update_input_player(int p)
{
	cont_state_t	*state;

	/* Get controller state */
	state = (cont_state_t *)maple_dev_status(devs[p]);
	if(!state)
		return 0;

	/* Buttons */
	input.pad[p] = state->buttons;
	if(state->ltrig > 128)
	{
		if ((state->rtrig > 128) && (state->buttons & CONT_START))
			return 1;
		else
			input.pad[p] |= INPUT_X;
	}
	if(state->rtrig > 128)
		input.pad[p] |= INPUT_Z;

	/* Exit combo was not pressed */
	return 0;
}

static __inline__ int update_input()
{
	int res = 0;
	int i = 0;

	while(devs[i])
		res |= update_input_player(i++);

	return res;
}

pvr_stats_t pvrstats;

void emulate_x(void)
{
	int draw_frame;

	do
	{
		/* Update controllers */
		if (update_input())
			break;

		/* Emulate one frame */
		if(!system_frame_x())
			system_reset();

		/* Update sound */
		if(snd.enabled)
		{
			draw_frame = dcsnd_update();
			if(!draw_frame)
			{
				frames_skipped++;
				if(frames_skipped > 1)
					draw_frame = 1;
			}
			else
				frames_skipped = 0;
		}
		else
			draw_frame = 1;

		/* Draw frame if no frameskipping is required */
		if(draw_frame)
		{
			/* Begin drawing a frame */
			pvr_wait_ready();
			pvr_scene_begin();

			/* Draw the borders */
			xrender_drawframe_op();

			#if 0
			/* Draw the on-screen display */
			if (dc_options.fps_display)
			{
				char fps_text[80];

				sprintf(fps_text, "RST: %03i/%03i/%03i FPS: %f\n",
					pvrstats.reg_last_time, pvrstats.rnd_last_time,
					pvrstats.frame_last_time, (double)pvrstats.frame_rate);

				pvr_list_begin(PVR_LIST_TR_POLY);

				fnt_write_simple(&font,
					 fps_text,
					 24, 24,
					 14,
					 0.5,
					 1,
					 0xFFFFFF);

				pvr_list_finish();
			}
			#endif

			/* Call the hardware renderer */
			xrender_drawframe();

			/* Finish drawing a frame, and send it off to the PVR */
			pvr_scene_finish();

			/* Shove the tiles off to the PVR */
			xrender_endframe();

			/* Grab the stats for the previous frame */
			#if 0
			if (dc_options.fps_display)
				pvr_get_stats(&pvrstats);
			#endif
		}
	}
	while(1);
}

void emulate(void)
{
	int draw_frame;
	pvr_vertex_t vert;

	do
	{
		/* Update controllers */
		if (update_input())
			break;
	
		/* Emulate one frame */
		if(!system_frame())
			system_reset();
	
		/* Update sound */
		if(snd.enabled)
		{
			dcsnd_update();
			frames_skipped++;
			if(frames_skipped & 1)
				draw_frame = 0;
			else
				draw_frame = 1;
		}
		else
			draw_frame = 1;
	
		/* Draw frame if no frameskipping is required */
		if(draw_frame)
		{
			/* Transfer the image buffer over */
			pvr_txr_load(screen_buffer, gen_texptr, TEX_WIDTH*TEX_HEIGHT*2);
			
			/* Begin drawing a frame */
			pvr_wait_ready();
			pvr_scene_begin();
		
			/* Start drawing the scene */
			pvr_list_begin(PVR_LIST_OP_POLY);
			
			/* Start drawing the rectangle */
			pvr_prim(&gen_hdr, sizeof(gen_hdr));
			vert.argb = PVR_PACK_COLOR(1.0f, 1.0f, 1.0f, 1.0f);
			vert.oargb = 0;
			vert.flags = PVR_CMD_VERTEX;
			
			/* Upper left vertex */
			vert.x = 0.0f;
			vert.y = 16.0f;
			vert.z = 1.0f;
			vert.u = SRC_LEFT;
			vert.v = SRC_TOP;
			pvr_prim(&vert, sizeof(vert));
			
			/* Upper right vertex */
			vert.x = 639.0f;
			vert.u = SRC_RIGHT;
			pvr_prim(&vert, sizeof(vert));
			
			/* Lower left vertex */
			vert.x = 0.0f;
			vert.y = 463.0f;
			vert.u = SRC_LEFT;
			vert.v = SRC_BUM;
			pvr_prim(&vert, sizeof(vert));
			
			/* Lower right vertex */
			vert.flags = PVR_CMD_VERTEX_EOL;
			vert.x = 639.0f;
			vert.u = SRC_RIGHT;
			pvr_prim(&vert, sizeof(vert));
			
			/* Draw the FPS counter */
			pvr_list_finish();
	
			/* Draw the on-screen display */
			#if 0
			if (dc_options.fps_display)
			{
				char fps_text[80];
				
				sprintf(fps_text, "RST: %03i/%03i/%03i FPS: %f\n",
					pvrstats.reg_last_time, pvrstats.rnd_last_time,
					pvrstats.frame_last_time, (double)pvrstats.frame_rate);
				
				pvr_list_begin(PVR_LIST_TR_POLY);
				
				fnt_write_simple(&font,
					 fps_text,
					 24, 24,
					 14,
					 0.5,
					 1,
					 0xFFFFFF);
			
				pvr_list_finish();
			}
			#endif
			
			/* Finish drawing a frame, and send it off to the PVR */
			pvr_scene_finish();
			
			#if 0
			/* Grab the stats for the previous frame */
			if (dc_options.fps_display)
				pvr_get_stats(&pvrstats);
			#endif
		}
	}
	while(1);
}

/* Main emulator loop */
/* TODO - Split out into separate parts for the menu. Again */
void test_exec(char *filename)
{
	int r;

	if( (dc_options.sound_enabled) && (!dcsnd_inited) )
		dcsnd_init();

	/* Load the ROM */
	r = load_rom(filename);
	if (r == 0)
		return;

	/* Initialise the Genesis */
	system_init(dc_options.cpu68k_type, dc_options.render_type);
	system_reset();

	/* Start jammin' */
	if (dc_options.sound_enabled)
		audio_init(SAMPLES_PER_SECOND);

	/* Activate all controllers attached */
	dc_scan_devices();
	for(r=0;r<2;r++)
	{
		if (devs[r])
			input.dev[r] = dc_options.fps_display ? DEVICE_6BUTTON : DEVICE_3BUTTON;
		else
			input.dev[r] = DEVICE_NONE;
	}
	io_rescan();

	/* Sound */
	if(snd.enabled)
		dcsnd_play();

	/* Enable appropriate main loop */
	switch(dc_options.render_type)
	{
		case 0:
			emulate();
			break;
		case 1:
			emulate_x();
			break;
	}

	/* Stop sound */
	if(snd.enabled)
	{
		dcsnd_stop();
		audio_shutdown();
	}

	/* Shutdown */
	system_shutdown();
}
